﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
using System.Runtime.CompilerServices;
using Cqrs.Common;

namespace Cqrs.Infrastructure.Aggregate
{
    public abstract class AggregateRootWithState : AggregateRoot
    {
        private StateMachine _stateMachine;
        private string _smfFileLocation;
        public string GetSmfFileLocation()
        {
            return _smfFileLocation;
        }

        private SmfFileParser _smfFileParser;
        public SmfFileParser GetSmfFileParser()
        {
            return _smfFileParser;
        }


        public StateMachine GetStateMachine()
        {
            return _stateMachine;
        }

        private void Initialise(XElement scxml)
        {
            _stateMachine = new StateMachine(scxml);
            CurrentState = _stateMachine[_stateMachine.InitialState];
        }

        public AggregateRootWithState()
        {
            ConstructWithSmfFile(GetDefaultSmfFile());
        }

        public AggregateRootWithState(string smfFile)
        {
            ConstructWithSmfFile(smfFile);
        }

        public AggregateRootWithState(XElement scxml)
        {
            if (scxml == null)
            {
                throw new ArgumentException("The scxml file value cannot be null");
            }
            Initialise(scxml);
        }

        protected void VerifyAssembly(Assembly assembly)
        {
            var stateMachine = this.GetStateMachine();
            foreach (var state in stateMachine)
            {
                foreach (var transition in state.Value.Transitions)
                {
                    VerifyCommandExistsInAssembly(assembly, transition.Value.Command);                    
                    VerifyEventExistsInAssembly(assembly, transition.Value.Event);
                }
            }
        }

        private void VerifyEventExistsInAssembly(Assembly eventsAssembly, string eventName)
        {

            var eventsWithState = eventsAssembly.GetExportedTypes().Where(p => p.IsSubclassOf(typeof(EventWithState))).ToList();
            if (eventsWithState == null)
                throw new ApplicationException(
                    string.Format("No EventsWithState classes were found in the {0} assembly.",
                    eventsAssembly.GetName(true).Name));

            var eventType = eventsWithState.FirstOrDefault(p => p.Name == eventName);
            if (eventType == null)
                throw new ApplicationException(
                    string.Format("The Event {0} was not found in the {1} assembly.",
                    eventName,
                    eventsAssembly.GetName(true).Name));

        }

        private void VerifyCommandExistsInAssembly(Assembly eventsAssembly, string commandName)
        {

            var command = eventsAssembly.GetExportedTypes().Where(p => p.IsSubclassOf(typeof(Command))).ToList();
            if (command == null)
                throw new ApplicationException(
                    string.Format("No Command classes were found in the {0} assembly.",
                    eventsAssembly.GetName(true).Name));

            var eventType = command.FirstOrDefault(p => p.Name == commandName);
            if (eventType == null)
                throw new ApplicationException(
                    string.Format("The Command {0} was not found in the {1} assembly.",
                    commandName,
                    eventsAssembly.GetName(true).Name));

        }

        private void ConstructWithSmfFile(string smfFile)
        {
            // todo - what if no Smf File has been set - error or default?
            if (string.IsNullOrEmpty(smfFile))
            {
                smfFile = GetDefaultSmfFile();
            }
            if (!System.IO.File.Exists(smfFile))
                throw new ArgumentException("The State Machine file: " + smfFile + " does not exist.  Please manually select the file.");

            _smfFileParser = new SmfFileParser(smfFile);
            _smfFileLocation = smfFile;
            XElement scxml = _smfFileParser.GetStateMachineXml();
            Initialise(scxml);
        }

        private string GetDefaultSmfFile()
        {
            string baseDirectory = Directory.GetParent(this.GetType().Assembly.Location).FullName;
            return Path.Combine(baseDirectory, this.GetType().Name + ".smf");
        }

        public State CurrentState
        {
            get;
            private set;
        }

        public State GetNextStateBasedOn(Type @event)
        {
            Transition nextTransition;
            State currentState;
            State nextState;

            if (!_stateMachine.TryGetValue(this.CurrentState.Name, out currentState))
                throw new ApplicationException(
                    string.Format(
                    "The {0} state does not exist in the State Machine", this.CurrentState.Name));

            if (!_stateMachine[this.CurrentState.Name]
                    .Transitions.TryGetValue(@event.Name, out nextTransition) == true)
                throw new ApplicationException(
                    string.Format(
                    "The event {0} is not allowed when in a State of {1}", @event.Name, this.CurrentState.Name));

            if (!_stateMachine.TryGetValue(nextTransition.ResultingState, out nextState))
                throw new ApplicationException(
                    string.Format(
                    "The {0} state does not exist in the State Machine", nextTransition));

            return nextState;
        }

        public override void LoadsFromHistory(IEnumerable<Event> history)
        {
            foreach (var @event in history)
            {
                if (@event.GetType().IsSubclassOf(typeof(EventWithState)))
                    ChangeState(((EventWithState)@event).NextState, forceChange: true);

                base.ApplyChange(@event, isNew: false);
                Version = @event.Version;
            }
        }

        protected void ApplyChange(EventWithState @event)
        {
            State state = GetNextStateBasedOn(@event.GetType());
            if (@event.GetType().IsSubclassOf(typeof(EventWithState)))
                ChangeState(state.Name, forceChange: false);
            @event.NextState = state.Name;
            ApplyChange(@event, isNew: true);
        }

        internal void ChangeState(string nextState, bool forceChange)
        {
            if (_stateMachine.Count(p => p.Key == nextState) > 0)
                this.CurrentState = _stateMachine[nextState];
            else
            {
                if (forceChange)
                {
                    // Even if the State doesn't exists in the 
                    // State Machine, we still need to assign it 
                    // to the Aggregate, as it has been stored
                    // in the the database.
                    this.CurrentState = new State(nextState);
                }
                else
                    throw new ApplicationException(
                        String.Format("The {0} state was not found in the State Machine.", nextState));

            }
        }

        public static AggregateRootWithState Load(Type assemblyType, string smfFile)
        {
            AggregateRootWithState aggregateRoot;
            try
            {
                Object[] parameters = new Object[] { smfFile };
                aggregateRoot =
                    (AggregateRootWithState)Activator.CreateInstance(assemblyType, parameters);
                return aggregateRoot;
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }
        }
    }
}
